Le tour du monde en quatre-vingt jours

Introduction

  • Ceci est un document présentant un test de lémmatisation avec R à partir du lexique Morphalou 3.1 de Ortolang.
  • Le texte utilisé est “Le tour du monde en quatre-vingt jours” de Jules Verne, récupéré en ligne sur le projet Gutenberg.
library(tidyverse)
library(tidytext)
# Le tour du monde en quatre-vingts jours
# book <- gutenberg_download(3456)

# récupérer le livre en latin1
book <- tibble(text = readr::read_lines('http://www.gutenberg.org/files/3456/3456-8.txt', 
                          locale = readr::locale(encoding = "latin1")))

# du début à la FIN du livre
# book[632:9888,] %>% 
#   mutate(text = stringi::stri_trans_general(text,"Latin-ASCII"))-> book_

book[632:9888,] -> book_

Découpage mot par mot et lemmatisation

Avec le package tidytext, on découpe mot par mot.

On applique une regexpr pour enlever les _ autour de _mot_ (ce qui correspond à l’italique dans les livres sur le projet gutenberg).

par_phrases <- book_ %>% 
  summarise(texte = paste0(text, collapse = " ")) %>% 
  mutate(texte = stringr::str_replace_all(texte, '\'', ' ')) %>% 
  unnest_tokens(phrases, texte, token = "sentences") %>% 
  mutate(phrase_number = row_number())

par_phrase_mot <- par_phrases %>% 
  unnest_tokens(word, phrases, token = "words") %>% 
  #mutate(word = stringr::str_extract(word, '[a-z0-9]+'))
  mutate(word = stringr::str_replace_all(word, '_', ''))

Quelques exemples de mots

book_ %>% 
  unnest_tokens(mot, text, token = "ngrams",  n = 1)  -> mots

glimpse(mots, width = 600)
Observations: 68,222
Variables: 1
$ mot <chr> "dans", "lequel", "phileas", "fogg", "et", "passepartout", "s'acceptent", "réciproquement", "l'un", "comme", "maître", "l'autre", "comme", "domestique", "en", "l'année", "1872", "la", "maison", "portant", "le", "numéro", "7", "de", "saville", "row", "burlington", "gardens", "maison", "dans", "laquelle", "sheridan", "mourut", "en", "1814", "était", "habitée", "par", "phileas", "fogg", "esq", "l'un", "des", "membres", "les", "plus", "singuliers", "et", "les", "plus", "remarqués", "du", "reform", "club", "de", "londres", "bien", "qu'il", "semblât", "prendre", "à", "tâche", "de", ...

Aperçu de la table lexique de Morphalou 3.1

On donne un aperçu sur 30 lignes tirées au sort dans le fichier, en sélectionnant certaines variables ici.

# lecture du fichier produit à partir du csv Morphalou 3.1
test <- readr::read_csv('results/tidy_morphalou3.1.csv.gz')

dtttable(
  sample_n(test %>% 
         select(forme_lemmatise, dictionnaire, 
                forme_flechie, GENRE_1, NOMBRE, 
                TEMPS, PERSONNE),
       30), n = 15
  )

Processus 1

On cherche à rapprocher la forme lémmatisée correspondant la plus proche du mot pour produire un étiquetage grammatical.

# Si le mot présente des formes fléchies, forme fléchie, sinon on place l'unique forme
# lemmatisée dans la forme fléchie
test <- test %>% 
  mutate(forme_flechie = case_when(
    is.na(forme_flechie) ~ forme_lemmatise,
    !is.na(forme_flechie) ~ forme_flechie
))

# nombre de caractères de la forme lemmatisée, on enlève les accents que l'on laissera sur la forme lemmatisée
test2 <- test %>% 
  mutate(nc = nchar(forme_lemmatise)) # %>% 
  # mutate(forme_flechie = stringi::stri_trans_general(forme_flechie,"Latin-ASCII"))


# Distance entre le mot orginal et la forme lemmatise
par_phrase_mot %>% 
  mutate(nc0 = nchar(word),
         rn = row_number()) %>% 
  left_join(test2, 
            by = c('word' = 'forme_flechie')) %>% 
  mutate(diff = stringdist::stringsim(word, forme_lemmatise)) %>% 
  arrange(rn, word, desc(diff)) %>% 
  distinct(rn, .keep_all = T)  %>% 
  mutate(word_2 = stringr::str_extract(word, '[a-z0-9]+')) %>% 
  filter(! stringr::str_detect('[a-z]{1}', word_2)) %>% 
  filter(! stringr::str_detect('[0-9]+', word_2)) %>% 
  filter(! word_2 %in% tm::stopwords('french')) -> words_lemmes

Nombre de mots / nombre de lemmes

words_lemmes %>% 
  summarise(nmots = n_distinct(word),
            nlemmes = n_distinct(forme_lemmatise)) %>% 
  knitr::kable()
nmots nlemmes
8316 5480

Aperçu des lemmes trouvés

On donne un aperçu sur quelques lignes tirées au sort dans le livre.

dtttable(sample_n(words_lemmes %>% 
                    filter(!is.na(forme_lemmatise)) %>% 
                    select(word, forme_lemmatise, dictionnaire, 
                GENRE_1, NOMBRE, 
                TEMPS, PERSONNE), 600), n = 15)

Aperçu des mots non lemmatisés

On donne un aperçu de mots non trouvés.

dtttable(count(words_lemmes %>% 
                    filter(is.na(forme_lemmatise)), 
               word, sort = T), n = 20)

Résultat de l’étiquetage grammatical

Dans un premier temps observons le résultat de l’étiquetage grammatical

Catégories

words_lemmes %>% 
  filter(!is.na(forme_lemmatise)) %>% 
  count(`CATÉGORIE`, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  head(20) %>% 
  knitr::kable()
CATÉGORIE n p
Nom commun 18801 0.5438531
Verbe 7186 0.2078681
Adjectif qualificatif 2929 0.0847266
Adverbe 2744 0.0793752
Nombre 901 0.0260631
Conjonction 677 0.0195835
Préposition 538 0.0155626
Pronom 473 0.0136824
Déterminant 214 0.0061903
Interjection 107 0.0030952

Verbes

Formes infinitives

words_lemmes %>% 
  filter(grepl('^verbe', dictionnaire)) %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.002, rot.per = 0)})

Mode, temps, personne

words_lemmes %>% 
  filter(grepl('^verbe', dictionnaire)) %>% 
  count(MODE, TEMPS, PERSONNE, sort = T) %>% 
  head(20) %>% 
  knitr::kable()
MODE TEMPS PERSONNE n
indicative imperfect thirdPerson 2332
indicative simplePast thirdPerson 1619
infinitive - - 1491
indicative present thirdPerson 432
indicative present secondPerson 240
participle past - 228
indicative present firstPerson 222
subjunctive imperfect thirdPerson 172
participle present - 144
conditional present thirdPerson 137
indicative future thirdPerson 48
indicative future firstPerson 40
indicative imperfect secondPerson 24
indicative future secondPerson 17
conditional present secondPerson 11
subjunctive present thirdPerson 7
imperative present secondPerson 5
indicative imperfect firstPerson 5
subjunctive present firstPerson 5
indicative simplePast secondPerson 4

Adverbes

words_lemmes %>% 
  filter(grepl('^adverbe', dictionnaire)) %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.002, rot.per = 0)})

Prépositions

words_lemmes %>% 
  filter(grepl('^préposition', dictionnaire)) %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.0015, rot.per = 0)})

Noms communs

words_lemmes %>% 
  filter(grepl('^nom commun', dictionnaire)) %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.002, rot.per = 0)})

Adjectifs qualificatifs

words_lemmes %>% 
  filter(grepl('^adjectif qualific', dictionnaire)) %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.002, rot.per = 0)})

N.B.: Passepartout est en fait l’un des personnages principaux du roman.

Pronoms

words_lemmes %>% 
  filter(grepl('^pronom', dictionnaire)) %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.0015, rot.per = 0)})

Déterminants

words_lemmes %>% 
  filter(grepl('^déterminant', dictionnaire)) %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.0015, rot.per = 0)})

Nombres

words_lemmes %>% 
  filter(!is.na(forme_lemmatise)) %>% 
  count(NOMBRE, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  head(20) %>% 
  knitr::kable()
NOMBRE n p
singular 20529 0.5938386
- 5817 0.1682673
plural 5811 0.1680937
invariable 2413 0.0698004

Genres

words_lemmes %>% 
  filter(!is.na(GENRE_1) & GENRE_1 != '-') %>% 
  count(GENRE_1, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  head(20) %>% 
  knitr::kable()
GENRE_1 n p
invariable 2156 0.4574581
masculine 2023 0.4292383
feminine 534 0.1133036

Processus 2

On cherche à rapprocher la forme lémmatisée sans produire d’étiquetage grammatical. On se contente de réduire le nombre de mots possibles (pluriel mis au singulier, féminin au masculin, formes conjuguées à l’infinitif, etc.)

# Si le mot présente des formes fléchies, forme fléchie, sinon on place l'unique forme
# lemmatisée dans la forme fléchie
test <- test %>% 
  mutate(forme_flechie = case_when(
    is.na(forme_flechie) ~ forme_lemmatise,
    !is.na(forme_flechie) ~ forme_flechie
))


test2 <- test %>% 
  mutate(nc = nchar(forme_lemmatise)) %>% 
  distinct(forme_flechie, forme_lemmatise, NOMBRE, GENRE, dictionnaire)


# Distance entre le mot orginal et la forme lemmatise
par_phrase_mot %>% 
  mutate(nc0 = nchar(word),
         rn = row_number()) %>% 
  left_join(test2, 
            by = c('word' = 'forme_flechie')) %>% 
  mutate(diff = stringdist::stringsim(word, forme_lemmatise)) %>% 
  arrange(rn, word, desc(NOMBRE), desc(diff)) %>%  # desc(GENRE),
  distinct(rn, .keep_all = T)  %>% 
  mutate(word_2 = stringr::str_extract(word, '[a-z0-9]+')) %>% 
  filter(! stringr::str_detect('[a-z]{1}', word_2)) %>% 
  filter(! stringr::str_detect('[0-9]+', word_2)) %>% 
  filter(! word_2 %in% tm::stopwords('french')) -> words_lemmes

Nombre de mots / nombre de lemmes

words_lemmes %>% 
  summarise(nmots = n_distinct(word),
            nlemmes = n_distinct(forme_lemmatise)) %>% 
  knitr::kable()
nmots nlemmes
8316 5504

Nuages de lemmes

words_lemmes %>% 
  count(forme_lemmatise, sort = T) %>% 
  mutate(p = n / sum(n)) %>% 
  (function(df){
    wordcloud::wordcloud(words = df$forme_lemmatise, freq = df$p, min.freq = 0.0012, rot.per = 0)})

Les formes lémmatisées

On compte les associations entre formes fléchies (mots du corpus) et formes lémmatisées (mots du lexique) lorsque les deux sont renseignés (lemme existant) et que l’une diffère de l’autre.

Table

count(words_lemmes %>% 
        filter(!is.na(forme_lemmatise), 
               word != forme_lemmatise), 
      word, forme_lemmatise, sort = TRUE) %>% 
  dtttable()

Graphes

n > 20 occurences

library(igraph)
library(ggraph)

count(words_lemmes %>% 
        filter(!is.na(forme_lemmatise), 
               word != forme_lemmatise), 
      word, forme_lemmatise, sort = TRUE) %>% 
  filter(n > 20) %>% 
  graph_from_data_frame() -> g

a <- grid::arrow(type = "closed", length = unit(.05, "inches"))

ggraph(g, layout = "fr") +
  geom_edge_link(aes(edge_alpha = n), show.legend = T,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "lightblue", size = 3) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1, size = 2) +
  theme_void() + 
  ggtitle("Formes fléchies - formes lémmatisées") -> p
p

11 < n < 21 occurences

count(words_lemmes %>% 
        filter(!is.na(forme_lemmatise), 
               word != forme_lemmatise), 
      word, forme_lemmatise, sort = TRUE) %>% 
  filter(n < 21, n > 11) %>% 
  graph_from_data_frame() -> g

a <- grid::arrow(type = "closed", length = unit(.05, "inches"))

ggraph(g, layout = "fr") +
  geom_edge_link(aes(edge_alpha = n), show.legend = T,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "lightblue", size = 3) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1, size = 2) +
  theme_void() + 
  ggtitle("Formes fléchies - formes lémmatisées") -> p
p

6 < n < 12 occurences

library(igraph)
library(ggraph)

count(words_lemmes %>% 
        filter(!is.na(forme_lemmatise), 
               word != forme_lemmatise), 
      word, forme_lemmatise, sort = TRUE) %>% 
  filter(n < 12, n > 6) %>% 
  graph_from_data_frame() -> g

a <- grid::arrow(type = "closed", length = unit(.03, "inches"))

ggraph(g, layout = "fr") +
  geom_edge_link(aes(edge_alpha = n), show.legend = T,
                 arrow = a, end_cap = circle(.03, 'inches')) +
  geom_node_point(color = "lightblue", size = 1) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1, size = 2) +
  theme_void() + 
  ggtitle("Formes fléchies - formes lémmatisées") -> p
p

Discussion

Sur le processus 1

  • Les mots non lémmatisés correspondent le plus souvent à des noms de personnages, des noms de lieux.
  • La méthode développée (rapprocher les formes fléchies des mots du corpus, sélectionner la forme lémmatisée minimisant la distance “stringdist” entre celui-ci et le mot d’origine) ne tient pas compte de la structure des phrases.
  • On observe donc dans les nuages de mots que l’étiquatage grammatical n’est pas satisfaisant : des verbes sont aussi des noms communs (avoir, dire, être). On peut aussi observer que certains adjectifs sont des verbes au participe présent (compris, trouvé), de même pour “or” et “car”. Il faudrait tenir compte de la structure morpho-syntaxique pour attribuer correctement le bon étiquetage grammatical à chaque mot, et ainsi tenir compte de l’ordonnocement des mots dans la phrase.
  • Ce problème ne se pose pas lorsqu’un mot ne correspond qu’à une seule étiquette grammaticale (la plupart des adverbes, pronoms et déterminants). Et l’approche proposée permet de réduire le nombre de formes dans un document. Étiqueter toutes les formes conjuguées d’un verbe en son infinitif permet déjà de réduire une part importante du nombre de formes.
  • Et étudier les structures morpho-syntaxique est d’une difficulté plus enlevée (ce que font de bons outils de lemmatisation !).

Sur le processus 2

  • Beaucoup plus simple, on s’attache à rattacher les singuliers et masulin / invariant aux mots.
  • On ne gagne en revanche aucune information grammaticale sur les mots.
  • Il y a des erreurs, qu’il faudrait quantifier à partir de la table de la sous-partie “formes lémmatisées”.
Logo AP-HP
Simap - DOMU

2017-12-05